"""
A circular queue is a linear data structure that follows the FIFO (First-In-First-Out) principle. It is implemented using an array or a list with a fixed size, where the front and rear pointers keep track of the current positions of the elements.

Properties:
- The circular queue has a fixed size and behaves like a circular structure, meaning that when the rear pointer reaches the end of the array, it wraps around to the beginning.
- It supports efficient insertion and deletion operations at both ends of the queue.
- The circular queue can be implemented using an array, which allows for efficient memory utilization.

Operations:

1. Initialization:
   - `CircularQueue(max_size)`: Creates an empty circular queue with the specified maximum size.

2. Basic Operations:
   - `enqueue(item)`: Inserts an element at the rear of the circular queue.
   - `dequeue()`: Removes and returns the element from the front of the circular queue.
   - `peek()`: Returns the element at the front of the circular queue without removing it.
   - `is_empty()`: Checks if the circular queue is empty.
   - `is_full()`: Checks if the circular queue is full.
   - `size()`: Returns the number of elements in the circular queue.

Example Usage:
```
queue = CircularQueue(5)

queue.enqueue(3)
queue.enqueue(7)
queue.enqueue(1)
print(queue.dequeue())  # Output: 3
print(queue.peek())  # Output: 7
print(queue.size())  # Output: 2
print(queue.is_empty())  # Output: False
print(queue.is_full())  # Output: False
```

Time and Space Complexity:
- The enqueue and dequeue operations have a time complexity of O(1), as they only involve updating the front and rear pointers.
- Peeking at the front element can also be done in constant time.
- Checking if the circular queue is empty or full, as well as obtaining its size, can be done in constant time.
- The space complexity of a circular queue implemented using an array is O(n), where n is the maximum size of the circular queue.
"""

class CircularQueue:
    """
    A simple implementation of a circular queue.

    This circular queue class supports enqueue, dequeue, peek, is_empty, is_full,
    clear, size, and get_capacity operations.

    Attributes:
        capacity (int): The maximum capacity of the circular queue.
    """

    def __init__(self, capacity: int = 10):
        """
        Initialize the circular queue.

        Args:
            capacity (int, optional): The maximum capacity of the circular queue. Defaults to 10.
        """
        self._data = [None] * capacity
        self._capacity = capacity
        self._front = 0
        self._rear = -1
        self._size = 0

    def __len__(self) -> int:
        """Return the number of elements in the circular queue."""
        return self._size

    def __repr__(self):
        """Return a string representation of the circular queue."""
        return f'CircularQueue(capacity={self._capacity}, data={self._data})'

    def __str__(self):
        """Return a formatted string representation of the circular queue."""
        return str(self._data)

    def enqueue(self, item: object) -> bool:
        """
        Add an item to the rear of the circular queue.

        Args:
            item (object): The item to be added.

        Returns:
            bool: True if the enqueue operation was successful, False otherwise.
        """
        if self.is_full():
            return False
        else:
            self._rear = (self._rear + 1) % self._capacity
            self._data[self._rear] = item
            self._size += 1
            return True

    def dequeue(self) -> object:
        """
        Remove and return the item from the front of the circular queue.

        Returns:
            object: The item from the front of the circular queue.

        Raises:
            IndexError: If the circular queue is empty.
        """
        if self.is_empty():
            raise IndexError('Circular queue is empty')
        item = self._data[self._front]
        self._data[self._front] = None
        self._front = (self._front + 1) % self._capacity
        self._size -= 1
        return item

    def peek(self) -> object:
        """
        Return the item from the front of the circular queue without removing it.

        Returns:
            object: The item from the front of the circular queue.

        Raises:
            IndexError: If the circular queue is empty.
        """
        if self.is_empty():
            raise IndexError('Circular queue is empty')
        return self._data[self._front]

    def is_empty(self) -> bool:
        """Check if the circular queue is empty."""
        return self._size == 0

    def is_full(self) -> bool:
        """Check if the circular queue is full."""
        return self._size == self._capacity

    def clear(self):
        """Clear all elements from the circular queue."""
        self._data = [None] * self._capacity
        self._front = 0
        self._rear = -1
        self._size = 0

    def size(self) -> int:
        """Return the current size of the circular queue."""
        return self._size

    def get_capacity(self) -> int:
        """Return the maximum capacity of the circular queue."""
        return self._capacity


if __name__ == "__main__":
    circular_queue = CircularQueue(5)  # Initialize a circular queue with capacity 5

    # Enqueue elements into the circular queue
    circular_queue.enqueue(3)
    circular_queue.enqueue(5)
    circular_queue.enqueue(6)
    circular_queue.enqueue(9)
    circular_queue.enqueue(10)

    # Try to enqueue an element when the circular queue is already at capacity
    assert not circular_queue.enqueue(100), "Expected enqueue to fail because circular queue is at capacity"

    print(circular_queue)  # Expected output: [3, 5, 6, 9, 10]
    assert len(circular_queue) == 5, "Expected circular queue length to be 5"
    assert circular_queue.peek() == 3, "Expected item at the front of the circular queue to be 3"

    # Dequeue an element
    item = circular_queue.dequeue()  # item = 3
    print(item)  # Expected output: 3

    # Peek at the front of the circular queue
    front_item = circular_queue.peek()  # front_item = 5
    print(front_item)  # Expected output: 5

    print(circular_queue)  # Expected output: [None, 5, 6, 9, 10]

    # Enqueue elements again after dequeue
    print(circular_queue.enqueue(11)) # Expected output: True
    print(circular_queue.enqueue(12)) # Expected output: False

    print("Circular Queue:", circular_queue)  # Expected output: Circular Queue: [11, 5, 6, 9, 10]
    print("Circular Queue length:", len(circular_queue))  # Expected output: Circular Queue length: 5

    # Check if the circular queue is empty or full
    print(circular_queue.is_empty())  # Expected output: False
    print(circular_queue.is_full())  # Expected output: False

    # Clear the circular queue
    circular_queue.clear()
    print(circular_queue)  # Expected output: [None, None, None, None, None]
    print(len(circular_queue))  # Expected output: 0

    # Check the current size and capacity of the circular queue
    print(circular_queue.size())  # Expected output: 0
    print(circular_queue.get_capacity())  # Expected output: 5
